import Popover from "./pop-utils";

import Sequence from "../core/sequence";
import WorkTask from "../events/work-task";

import Rect, {Align, Direction} from "./rect";

const sequence = new Sequence(100);

/**
 * 一个定时任务
 */
const workTask = new WorkTask();

/**
 * 可复用的吸附窗口
 *
 * 通过不断刷新界面实现的气泡窗口，
 *
 * 目前项目内有很多可选方案，
 *
 * 除非没得选了，不然不要选择这一套实现方案。
 */
class UltimatePopover {
    /**
     * 弹窗元素
     */
    private dom: HTMLElement;
    /**
     * 吸附元素
     */
    private ref: HTMLElement;
    /**
     * 弹窗元素
     */
    private box: HTMLElement;
    /**
     * 弹窗区域
     */
    private dom_rect: Rect;
    /**
     * 吸附区域
     */
    private ref_rect: Rect;
    /**
     * 限制区域，超出限制区域之后，隐藏组件
     */
    private box_rect: Rect;

    /**
     * 对齐方式
     */
    align: Align = 'start';
    /**
     * 相对于 ref 的方向
     */
    direction: Direction = 'bottom';
    /**
     * 边缘留白空间
     */
    offset: number = 10;
    /**
     * 降级处理
     *
     * 超出 box 限制区域之后的方向：默认调转方向
     */
    fallback: Direction | 'reverse' = 'reverse';
    /**
     * 降级处理失败之后的解决方案
     *
     * - hidden: 隐藏
     * - remove: 移除
     * - ignore: 使用备选方案展示
     */
    fallback2: 'hidden' | 'remove' | 'ignore' = 'ignore';

    /**
     * 是否正在打开
     */
    opening: false;

    /**
     * 依附组件不可见
     */
    out_of_box: false;
    /**
     * 当前组件的方向
     */
    current_direction: undefined;

    /**
     * 方向变化时间
     */
    onDirectionChange: Function;

    constructor(dom) {
        if (dom !== undefined) {
            this.setDom(dom);
        }
    }

    setDom(dom) {
        this.dom = dom;
        this.dom_rect = new Rect(dom);
    }

    setRef(dom) {
        this.ref = dom;
        this.ref_rect = new Rect(dom);
    }

    setBox(dom) {
        this.box = dom;
        this.box_rect = new Rect(dom);
    }

    setRefRect(rect) {
        this.ref_rect = rect;
    }

    setBoxRect(rect) {
        this.box_rect = rect;
    }

    /**
     * 设置 index
     *
     * @param index
     */
    setZIndex(index) {
        this.dom.style.zIndex = index;
    }

    /**
     * 使用内置的自增序列作为 z-index
     */
    withNextZIndex() {
        this.setZIndex(sequence.next());
    }

    /**
     * 设置 z-index，使之能覆盖提供的 dom 对象。
     *
     * @param d {HTMLElement}   用于比较的 dom 对象
     * @param def {number}      默认的 z-index
     */
    cover(d, def = undefined) {
        let index = DomUtils.getZIndex(d);
        if (index === -1) {
            if (def === undefined) {
                // 使用自增序列
                index = sequence.next();
            } else {
                // 使用指定的默认值
                index = def;
            }
        } else {
            // 尝试从提供的 dom 组件
            index++;
        }
        this.setZIndex(index);
    }

    /**
     * 关闭组件
     *
     * 实际只是放到了屏幕之外，这里不能真的隐藏，
     *
     * 如果隐藏组件，很多属性无法获取。
     */
    close() {
        this.dom.style.left = '-1000px';
        this.dom.style.top = '-1000px';
        this.opening = false;
    }

    /**
     * 隐藏组件
     *
     * 状态仍然为打开，不过界面上看不到当前窗口
     *
     * 超出限制区域之后，需要隐藏组件
     */
    hidden() {
        this.dom.style.left = '-1000px';
        this.dom.style.top = '-1000px';
    }

    /**
     * 将 dom 组件展示在指定的位置
     *
     * 绝对定位，不考虑其它限制条件
     *
     * @param location 相对于屏幕的位置 [left, top]
     * @param direction 方向
     */
    open(location: [number, number], direction: Direction) {
        this.dom.style.left = location[0] + 'px';
        this.dom.style.top = location[1] + 'px';
        this.opening = true;

        this.dom_rect.ofDOM(this.dom);

        this.setDirection(direction);
    }


    /**
     * 将 dom 组件，放置在 ref 的相对位置。
     *
     * E.G.: 各类吸附窗口
     *
     * @param ref - 定位组件
     * @param direct - 弹窗方向
     * @param align - 对齐方式
     * @param offset - 偏移量，弹窗与组价之间的间隙
     * @returns 由 left, top 构成的数组
     */
    openOf(ref: Rect, direct: Direction, align: Align, offset: number = 0): [number, number] {
        const location = Popover.openOf(this.dom_rect, ref, direct, align, offset);
        this.open(location);
    }

    /**
     * 将 dom 组件，放置在 box 内部。
     *
     * E.G.: 主页的消息弹窗，一般位于 body 的 bottom-end 位置
     *
     * @param box - 限制范围
     * @param direct - 弹窗方向
     * @param align - 对齐方式
     * @param offset - 偏移量，弹窗与组价之间的间隙
     * @returns 由 left, top 构成的数组
     */
    openIn(box: Rect, direct: Direction, align: Align, offset: number = 0): [number, number] {
        const location = Popover.openIn(this.dom_rect, box, direct, align, offset);
        this.open(location);
    }


    /**
     * 右下角弹窗
     *
     * E.G.：右击菜单，浏览器右击的时候，右下角弹出菜单面板，如果达到临界值，则将菜单展示在左下角（相对于点击位置）
     *
     * 自动微调属性，始终保证 dom 不会展示在容器外面。
     *
     * 这是简化逻辑后的一个函数，其它函数也能实现这一效果。
     *
     * @param box - 限制范围
     * @param left - 相对于屏幕的 left
     * @param top  - 相对于屏幕的 top
     */
    openInRightBottom(box: Rect, left: number, top: number): [number, number] {
        const location = Popover.openInRightBottom(this.dom_rect, box, left, top);
        this.open(location);
    }

    /**
     * 将 dom 组件，放置在 ref 的相对位置，并且限制在 box 内部。
     *
     * E.G.: 各类吸附窗口
     *
     * @param ref - 定位组件
     * @param box - 限制范围
     * @param direct - 弹窗方向
     * @param align - 对齐方式
     * @param offset - 偏移量，弹窗与组价之间的间隙
     */
    openInBoxOf(ref: Rect, box: Rect, direct: Direction, align: Align, offset: number = 0): [number, number] {
        const location = Popover.openInBoxOf(this.dom_rect, ref, box, direct, align, offset);
        this.open(location);
    }


    /**
     * 弹窗方向，dom 相对于 ref 的方向
     *
     * @param ref - 定位组件的坐标
     * @param direct - 弹窗方向，dom 相对于 ref 的方向
     * @returns 弹窗方向，dom 相对于 ref 的方向
     */
    getDirection = (ref: [number, number], direct: Direction): Direction => {
        if (direct === "top" || 'bottom') {
            return this.dom_rect.top < ref[1] ? "top" : "bottom";
        } else {
            return this.dom_rect.left < ref[0] ? "left" : "right";
        }
    }

    /**
     * 计算组件备选方向
     * @returns 备选方向
     */
    fallbackDirection(): Direction {
        if (this.fallback === 'reverse') {
            if (this.direction === 'top') {
                return 'bottom'
            } else if (this.direction === 'bottom') {
                return 'top'
            } else if (this.direction === 'left') {
                return 'right'
            } else {
                return 'bottom'
            }
        } else {
            return this.fallback;
        }
    }

    /**
     * 设置方向
     *
     * @param direction - 方向
     */
    setDirection(direction: Direction) {
        if (this.current_direction !== direction) {
            this.current_direction = direction;
            if (this.onDirectionChange !== undefined) {
                this.onDirectionChange(direction);
            }
        }
    }

    /**
     * 刷新弹窗
     *
     * 根据矩形块的属性，重新设置窗口的位置
     */
    render() {
        // 初选位置
        let location = Popover.openOf(this.dom_rect, this.ref_rect, this.direction, this.align, this.offset);
        if (this.box_rect == null) {
            this.open(location, this.direction);
        } else {
            // 区域检测 1
            const out = Popover.isOutOfBox(location, this.dom_rect, this.box_rect);
            if (out) {
                // 设置备选区域
                const direction = this.fallbackDirection();
                location = Popover.openOf(this.dom_rect, this.ref_rect, direction, this.align, this.offset);

                if (this.fallback2 === 'ignore') {
                    this.open(location, direction);
                } else {
                    // 区域检测 2
                    this.out_of_box = Popover.isOutOfBox(location, this.dom_rect, this.box_rect);
                    if (this.out_of_box) {
                        // 组件仍然超出限制区域
                        this.fallback2 === 'remove' ? this.close() : this.hidden();
                        this.setDirection(null);
                    } else {
                        // 使用备选区域展示
                        this.open(location, direction);
                    }
                }
            } else {
                // 正常展示
                this.open(location, this.direction);
            }
        }
    }

    /**
     * 深度刷新
     *
     * 重新获取 dom 参数，之后刷新弹窗，
     *
     * 使用场景：组件会发生变化，需要刷新位置。
     */
    refresh(): void {
        this.dom_rect.ofDOM(this.dom);
        if (this.ref) {
            this.ref_rect.ofDOM(this.ref);
        }
        if (this.box) {
            this.box_rect.ofDOM(this.box);
        }
        this.render();
    }

    /**
     * 定时任务
     *
     * 专门用于实时刷新组件位置的函数
     */
    schedule() {
        if (this.opening && this.ref_rect) {
            const pre = this.ref_rect.getLocation();
            if (this.ref) {
                this.ref_rect.ofDOM(this.ref);
            }
            const cur = this.ref_rect.getLocation();
            if (cur[0] !== pre[0] || cur[1] !== pre[1]) {
                if (this.box) {
                    this.box_rect.ofDOM(this.box);
                }
                this.render();
            }
        }
    }

    /**
     * 当前对象绑定到定时侦听任务
     *
     * 基本原理：启动一个 10 毫秒的定时刷新任务，不断刷新组件的位置信息；
     *
     * 缺点：启动一个侦听线程，有些浪费机器性能；
     * 优点：功能通用，不会产生事件冲突等问题，兼容各种奇奇怪怪的动画特效。
     */
    bindInterval() {
        const my = this;
        this.onchange = () => {
            my.schedule();
        }
        workTask.addListener(this.onchange);
    }

    /**
     * 接触定时侦听
     */
    unbindInterval() {
        workTask.removeListener(this.onchange);
    }

    /**
     * 是否属于 dom 组件的点击事件
     *
     * 弹窗展开后，一般会要求，点击任何位置，关闭窗口。
     *
     * 两个组件例外：
     * - 当前窗口；
     * - 触发窗口的组件；
     */
    isReferenceClick(evt: Event) {
        return this.dom.contains(evt.target) || this.ref_rect === evt.currentTarget;
    }
}

export default UltimatePopover;


